package com.ruiyou.uu.encryption;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;

/**
 * Author: Ruiyou
 * Time: 2023/7/19
 * Description:
 * This class could used for all platform run in JVM.
 */
@SuppressWarnings({
        "CharsetObjectCanBeUsed", "ResultOfMethodCallIgnored"
})
public class EncryptionUtil {
    private static final String YJM = ".yjm";
    private static final String AMD_PW = "asdfg";
    private static final int MAX_LENGTH = 4096;//最大加密大小
    private static final int R_OK = 0;
    private static final int R_ERR = 1;
    private static final int R_SKIP = -1;
    private static final int MAX_ERR_C = 5;
    private static final boolean isWinOS;

    static {
        String name = System.getProperty("os.name");
        if (name == null)
            isWinOS = false;
        else
            isWinOS = name.contains("Windows");
    }

    private final SendMsg sendMsg;
    private int sucC, errC;

    public EncryptionUtil(SendMsg sendMsg) {
        this.sendMsg = sendMsg;
    }

    interface SendMsg {
        void accept(String msg);
    }

    //验证密码合法性
    private boolean isPwValid(String pw) {
        if (pw.length() != 6)
            return false;
        for (int i = 0; i < pw.length(); i++) {
            if (!Character.isLetterOrDigit(pw.charAt(i)))
                return false;
        }
        return true;
    }

    //加密过程
    private int encrypt(File sourceFile, String pw) {
        if (sourceFile.getAbsolutePath().endsWith(YJM))
            return R_SKIP;
        sendMsg.accept("Encrypting: " + sourceFile.getPath());
        //加密
        try (RandomAccessFile ranFile =
                     new RandomAccessFile(sourceFile, "rw")) {
            long fLen = sourceFile.length();
            byte[] buf = new byte[(int) (
                    fLen < MAX_LENGTH ? fLen : MAX_LENGTH)];
            ranFile.read(buf);
            encryptBytes(buf, pw);
            ranFile.seek(0);
            ranFile.write(buf);
            //封装文件名，密码
            ranFile.seek(fLen);
            buf = sourceFile.getName().getBytes("UTF-8");
            ranFile.write(buf); // filename
            ranFile.writeShort(buf.length); // filename length
            buf = pw.getBytes("UTF-8");
            encryptBytes(buf, AMD_PW);
            ranFile.write(buf); // passwords
            ranFile.write(buf.length);
            ranFile.write(0); // error count
            ranFile.close();
            //重命名
            File destFile;
            do {
                destFile = new File(
                        sourceFile.getParent(),
                        String.format(
                                ".%04x%04x%s",
                                (int) (Math.random() * 0xFFFF),
                                (int) (Math.random() * 0xFFFF),
                                YJM
                        )
                );
            } while (destFile.exists());
            sourceFile.renameTo(destFile);
            if (isWinOS)
                Runtime.getRuntime().exec(
                        "cmd /Q /C attrib +H +S " + destFile.getPath()
                );
            return R_OK;
        } catch (IOException e) {
            sendMsg.accept("\tError: " + e.getMessage());
            return R_ERR;
        }
    }

    //解密过程
    private int decrypt(File sourceFile, String pw) {
        if (!sourceFile.getName().endsWith(YJM)) return R_SKIP;
        sendMsg.accept("Decrypting: " + sourceFile.getPath());
        try (RandomAccessFile ranFile =
                     new RandomAccessFile(sourceFile, "rw")) {
            long fLen = sourceFile.length();
            byte[] buf = pw.getBytes("UTF-8");
            int pwbLen = buf.length;
            ranFile.seek(fLen - pwbLen - 4);
            int fnbLen = ranFile.readShort();
            ranFile.read(buf);
            encryptBytes(buf, AMD_PW);
            if (ranFile.read() != pwbLen ||
                    !new String(buf, "UTF-8").equals(pw)) {
                int errC = ranFile.read() + 1;
                sendMsg.accept("\tPassword Error count: " + errC +
                        "\n\tRemain " + (MAX_ERR_C - errC) +
                        " times to try."
                );
                if (errC >= MAX_ERR_C) {
                    sendMsg.accept("\tPassword error count equals " +
                            MAX_ERR_C + ", delete file now."
                    );
                    ranFile.close();
                    sourceFile.delete();
                } else {
                    ranFile.seek(fLen - 1);
                    ranFile.write(errC);
                }
                return R_ERR;
            }
            //解密，还原文件名
            ranFile.seek(fLen - pwbLen - fnbLen - 4);
            buf = new byte[fnbLen];
            ranFile.read(buf);
            File destFile = new File(sourceFile.getParent(),
                    new String(buf, "UTF-8"));
            ranFile.setLength(fLen - pwbLen - fnbLen - 4);
            fLen = ranFile.length();
            buf = new byte[(int) (
                    fLen < MAX_LENGTH ? fLen : MAX_LENGTH)];
            ranFile.seek(0);
            ranFile.read(buf);
            encryptBytes(buf, pw);
            ranFile.seek(0);
            ranFile.write(buf);
            ranFile.close();
            sourceFile.renameTo(destFile);
            if (isWinOS)
                Runtime.getRuntime().exec(
                        "cmd /Q /C attrib -H -S " + destFile.getPath()
                );
            return R_OK;
        } catch (IOException e) {
            sendMsg.accept("\tError：" + e.getMessage());
            return R_ERR;
        }
    }

    //加密字节数组
    void encryptBytes(byte[] buf, String pw) {
        try {
            byte[] pwBytes = pw.getBytes("UTF-8");
            for (int i = 0, j = 0; i < buf.length; i++, j++) {
                if (j >= pwBytes.length) j = 0;
                buf[i] ^= pwBytes[j];
            }
        } catch (UnsupportedEncodingException ignore) {
        } // never to throw
    }

    private void processDir(
            File dir, boolean isRecurs, String pw, boolean isEncrypt) {
        File[] files = dir.listFiles(File::isDirectory);
        if (isRecurs && files != null)
            for (File f : files)
                processDir(f, true, pw, isEncrypt);
        files = dir.listFiles(File::isFile);
        if (files == null)
            return;
        for (File f : files) {
            int r = isEncrypt ? encrypt(f, pw) : decrypt(f, pw);
            if (r == R_OK)
                sucC++;
            else if (r == R_ERR)
                errC++;
        }
    }

    /**
     * Retrieve current success count.
     *
     * @return the success int value.
     */
    public int getSucC() {
        return sucC;
    }

    /**
     * Starting encrypt or decrypt the directory's children files
     *
     * @param dir       directory
     * @param isRecurs  recursive the dir
     * @param pw        password
     * @param isEncrypt true to encrypt or false to decrypt
     */
    public void start(
            File dir, boolean isRecurs, String pw, boolean isEncrypt) {
        if (!dir.exists()) {
            sendMsg.accept("Dir not found: " + dir.getPath());
            return;
        }
        if (!isPwValid(pw)) {
            sendMsg.accept("Passwords Format Error: Must combine " +
                    "with 6 letter or number");
            return;
        }
        sucC = errC = 0;
        processDir(dir, isRecurs, pw, isEncrypt);
        sendMsg.accept("\nSucceed " + sucC + " files." +
                "\nAnd " + errC + " files error.");
    }
}
